using System;
using System.Collections.Generic;

using TrueCode.Core.Aspects;


namespace TrueCode.DependencyManager
{
  /// <summary>
  /// Represents a collection of keys and values where type of the value is a part of key.
  /// Thus it is possible to associate more than one value with one key if type of values differ.
  /// </summary>
  /// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
  /// <typeparam name="TValueBase">The type of the values in the dictionary.</typeparam>
  public class TypedValueDictionary< TKey, TValueBase >
  {
    private readonly Dictionary< Key, TValueBase > _map = new Dictionary< Key, TValueBase >();

    #region Add
    /// <summary>
    /// Adds the specified key and value to the dictionary.
    /// </summary>
    /// <typeparam name="TValue">The type of the value. This type is used as a part of key, <see cref="Get"/> method.</typeparam>
    /// <param name="key">The key of the element to add.</param>
    /// <param name="value">The value of the element to add. The value can be null.</param>
    public void Add< TValue >( TKey key, TValueBase value ) where TValue : TValueBase
    {
      Add( typeof( TValue ), key, value );
    }

    /// <summary>
    /// Adds the specified key and value to the dictionary.
    /// </summary>
    /// <param name="typeofValue">The type of the value. This type is used as a part of key, <see cref="Get"/> method.</param>
    /// <param name="key">The key of the element to add.</param>
    /// <param name="value">The value of the element to add. The value can be null.</param>
    public void Add( Type typeofValue, TKey key, TValueBase value )
    {
      #region Assert that TValueBase is assignable from type specified in typeofValue
      if( ! typeof( TValueBase ).IsAssignableFrom( typeofValue ) )
      {
        var exc = new ArgumentException( "TValueBase is not assignable from typeofValue", "typeofValue" );
        exc.Data.Add( "TypeofValue", typeofValue );
        throw exc;
      }
      #endregion

      _map.Add( new Key( typeofValue, key ), value );
    }
    #endregion

    #region Get
    /// <summary>
    /// Gets the value associated with the specified key.
    /// </summary>
    /// <typeparam name="TValue">The type of the value. This type is used as a part of key, <see cref="Add"/> method.</typeparam>
    /// <param name="key">The key of the value to get.</param>
    /// <returns>The value associated with the specified key or null if the specified key is not found.</returns>
    public TValue Get< TValue >( TKey key ) where TValue : class, TValueBase
    {
      return ( TValue )Get( typeof( TValue ), key );
    }


    /// <summary>
    /// Gets the value associated with the specified key.
    /// </summary>
    /// <param name="typeofValue">The type of the value. This type is used as a part of key, <see cref="Add"/> method.</param>
    /// <param name="key">The key of the value to get.</param>
    /// <returns>The value associated with the specified key or null if the specified key is not found.</returns>
    public TValueBase Get( Type typeofValue, TKey key )
    {
      #region Assert that TValueBase is assignable from type specified in typeofValue
      if( ! typeof( TValueBase ).IsAssignableFrom( typeofValue ) )
      {
        var exc = new ArgumentException( "TValueBase is not assignable from typeofValue", "typeofValue" );
        exc.Data.Add( "TypeofValue", typeofValue );
        throw exc;
      }
      #endregion

      TValueBase value;
      _map.TryGetValue( new Key( typeofValue, key ), out value );
      return value;
    }
    #endregion

    #region Remove
    /// <summary>
    /// Removes the value with the specified key.
    /// </summary>
    /// <typeparam name="TValue">>The type of the value. This type is used as a part of key, <see cref="Add"/> method.</typeparam>
    /// <param name="key">>The key of the value to get.</param>
    /// <returns>Returns the value associated with the specified key if the element is successfully found and removed; 
    /// otherwise, null. This method returns null if key is not found.</returns>
    public TValue Remove< TValue >( TKey key ) where TValue : TValueBase
    {
      return ( TValue )Remove( typeof( TValue ), key );
    }

    /// <summary>
    /// Removes the value with the specified key.
    /// </summary>
    /// <param name="typeofValue">The type of the value. This type is used as a part of key, <see cref="Add"/> method.</param>
    /// <param name="key">>The key of the value to get.</param>
    /// <returns>Returns the value associated with the specified key if the element is successfully found and removed; 
    /// otherwise, null. This method returns null if key is not found.</returns>
    public TValueBase Remove( Type typeofValue, TKey key )
    {
      #region Assert that TValueBase is assignable from type specified in typeofValue
      if( ! typeof( TValueBase ).IsAssignableFrom( typeofValue ) )
      {
        var exc = new ArgumentException( "TValueBase is not assignable from typeofValue", "typeofValue" );
        exc.Data.Add( "TypeofValue", typeofValue );
        throw exc;
      }
      #endregion

      TValueBase value;
      var compositeKey = new Key( typeofValue, key );
      if( _map.TryGetValue( compositeKey, out value ) )
        _map.Remove( compositeKey );
      return value;
    }
    #endregion

    #region Key nested class
    /// <summary>
    /// Instances of this class are used as keys in the <see cref="_map"/> dictionary.
    /// </summary>
    private class Key : IEquatable< Key >
    {
      #region Fields
      [CanBeNull] private readonly TKey _key;
      [NotNull] private readonly Type _typeofValue;
      #endregion

      #region .ctors
      /// <summary>
      /// Initializes a new instance of the <see cref="Key"/> class.
      /// </summary>
      /// <param name="typeofValue">The type of the value stored in the dictionary.</param>
      /// <param name="key">Extern key to associate the value with it. Can be null.</param>
      public Key( [NotNull] Type typeofValue, [CanBeNull] TKey key )
      {
        if( typeofValue == null ) throw new ArgumentNullException( "typeofValue" );

        _key = key;
        _typeofValue = typeofValue;
      }
      #endregion

      #region Implementation of IEquatable< Key >
      public bool Equals( Key other )
      {
        return Equals( _key, other._key ) && Equals( _typeofValue, other._typeofValue );
      }
      #endregion

      #region Implementation of object equality
      public override bool Equals( object obj )
      {
        return Equals( obj as Key );
      }

      public override int GetHashCode()
      {
        return ( Equals( _key, null ) ? 0 : _key.GetHashCode() ) ^ _typeofValue.GetHashCode();
      }
      #endregion
    }
    #endregion
  }
}